如果給定一個值如 0x67a9,這到底是「指標」還是「(十六進制的)整數」型別呢?
答案是兩種都有可能,而且兩型別之間可以互相轉換,本篇文章就來細細探討這是怎麼回事。
以下列程式碼而言:
int main(){
int *ptr = (int *)0x67a9;
*ptr = 0xaa55;
}
首先來看這個 main() 函數中的第一行:
int *ptr:宣告一個名為 ptr 的指標變數,指標指向一個整數型別。(int *)0x67a9:0x67a9 本來是一個十六進位的數字,強制轉換成一個記憶體位址的值,該記憶體位址裝的是整數型別。總之,現在 0x67a9 是一個記憶體位址,該記憶體位址存著一個整數,而指向該記憶體位址的指標變數名為 ptr。
+-------------+
ptr -> | int 型別變數 |
+-------------+
記憶體位址 0x67a9
接著來看第二行:
*ptr = 0xaa55;
*ptr:為 ptr 這個變數解索引。0xaa55:要放進 ptr 指向之記憶體位址的值。所以會變成這樣:
+------------+
ptr -> | 0xaa55 |
+------------+
記憶體位址 0x67a9
整體而言,本程式碼的重點是原為整數的 0x67a9 經 (int *) 做了型別轉換,變成一個記憶體位址,然後再把 0xaa55 這個值放進該記憶體位址。
這邊未完待續,到第三段我們還會再回頭看這樣寫有什麼問題。
除了可以將整數轉為指標,也可以把指標轉為整數。
以下列範例而言:
int a = 100;
int *ptr = &a; // ptr 是指向 a 變數記憶體位址的指標
int address_value = (int)ptr; // 把 ptr 指標變數型別改為 int
看起來很平常對吧?但事實上這樣寫非常危險!
因為在現代 64 位元電腦中,整數型別只佔 32 位元、但指標佔了 64 位元,佔不同位元的型別互相轉換可能會讓資料本身跑掉,因此要用 intptr_t 或無正負號的 uintptr_t 比較安全,範例如下:
#include <iostream>
#include <stdint.h> // uintptr_t 放在 <stdint.h> 裡面
int main()
{
int a = 100;
int *ptr = &a;
// 因為指標不會是負數,因此用 uintptr_t 而非 intptr_t
uintptr_t address_value = (uintptr_t)ptr;
return 0;
}
intptr_t 跟 uintptr_t 會自動調配位元,避免資料異動。
指標轉整數會因為型別位元數不同而危險,但整數轉指標難道不會有相同問題?
其實也不安全!因此一樣要派出 uintptr_t,且轉換時使用 reinterpret_cast,使程式的閱讀者能更清楚看出是在進行風險較高的型別轉換:
#include <iostream>
#include <cstdint> // uintptr_t 在 <cstdint> 裡面
int main() {
uintptr_t address = 0x67a9; // 0x67a9 依然是一個整數型別,但用 uintptr_t 取代 int
// 用 reinterpret_cast 將整數變數轉換為指標
// 另外用 volatile 確保編譯器處理 ptr 指向的 int 時,不會使用暫存檔
volatile int* ptr = reinterpret_cast<volatile int*>(address);
*ptr = 0xaa55; // 解引用並寫入數值
return 0;
}
其中這行比較關鍵,可以拆開來看:
volatile int* ptr = reinterpret_cast<volatile int*>(address);
volatile int* ptr:ptr 是一個指向 volatile int 的指標。reinterpret_cast:表示要把後面的 address 從 uintptr_t 型別強制轉成 <volatile int*> 型別。所以整體而言就是「reinterpret_cast 把原為 uintptr_t 型別的變數 address 轉為指向 volatile int* 型別之指標 ptr」,後面再解索引說指標指向的值為 0xaa55。